/*******************************************************************************
 * Copyright (c) 2009  Clark N. Hobbie
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Clark N. Hobbie - initial API and implementation
 *******************************************************************************/
package org.eclipse.ecf.ipc.fifo;

import java.io.IOException;

import org.eclipse.ecf.ipc.IPCException;
import org.eclipse.ecf.ipc.IPCPackage;
import org.eclipse.ecf.ipc.Utils;


/**
 * An internal class that provides the underlying system calls needed to implement 
 * named pipes.
 * <P>
 * This class is implementation dependent and not intended for general use.
 * </P>
 * 
 * @author Clark N. Hobbie
 */
public class FIFOImpl
{
	static {
		IPCPackage.initializePackage();
	}
	
	public static final int DIRECTION_READER = 0;
	public static final int DIRECTION_WRITER = 1;

	public static final int BLOCKING_MODE_BLOCKING = 1;
	public static final int BLOCKING_MODE_NON_BLOCKING = 2;
	
	public static final int SELECT_READING = 1;
	public static final int SELECT_WRITING = 2;
	public static final int SELECT_BOTH = 3;

	/**
	 * The role a client is taking (reader/writer) when using a named pipe. <H2>NOTE</H2>
	 * This class really belongs in {@link FIFO} and will probably be moved there in
	 * the near future.
	 * 
	 * @author Clark N. Hobbie
	 */
	public enum PipeDirection
	{
		Reader(DIRECTION_READER), Writer(DIRECTION_WRITER);

		public int jniValue;

		private PipeDirection(int direction)
		{
			jniValue = direction;
		}
	}

	public FIFOImpl(String virtualName)
	{
		initialize(virtualName);
	}
	
	protected void initialize(String virtualName)
	{
		setVirtualName(virtualName);
	}
	
	
	private native void createImpl(FIFOResult result);

	public FIFOResult create() throws IPCException
	{
		setCreator(true);

		processVirtualActualFile();
		FIFOResult result = new FIFOResult();
		createImpl(result);

		if (result.resultCode == FIFOResult.SUCCESS)
		{
			setHandle(result.handle);
		}
		
		return result;
	}
	
	private void processVirtualActualFile() throws IPCException
	{
		String suffix = Long.toString(System.currentTimeMillis());
		
		//
		// if the platform DOES NOT have any particular requirements for the name
		// of a pipe, then use the virtual name as the actual name.
		//
		String actual = toActualName(suffix);
		if (null == actual)
		{
			actual = getVirtualName();
		}
		
		//
		// otherwise, write the actual name into the virtual file.
		//
		else
		{
			try
			{
				actual = Utils.readOrCreate(getVirtualName(), actual);
			}
			catch (IOException e)
			{
				String msg =
					"Error trying to read/write virtual file: " + getVirtualName();
				throw new IPCException(msg,e);
			}
		}
		
		setActualName(actual);
	}

	private native void writeImpl(FIFOResult result, byte[] buffer, int offset,
			int length, int timeoutMsec);

	/**
	 * Try to write a buffer of data within the specified time frame.
	 * <P>
	 * It is possible for a FIFO to have enough data written to it that it runs
	 * out of buffer space. When that happens the writer must wait until the
	 * reader reads enough of the data to free up some more buffer space.
	 * </P>
	 * 
	 * <P>
	 * The timeout specifies how long the caller is willing to wait for buffer
	 * space to free up. A value of less than 0 means that the caller will wait
	 * "forever." A value of 0 means that the caller will not wait at all --- if
	 * it is not possible to write immediately, then throw a timeout.
	 * </P>
	 * 
	 * <P>
	 * Any other value specifies the number of milliseconds that the caller will
	 * wait for the space to free up.
	 * </P>
	 * 
	 * <P>
	 * Note that a wait will only occur if the FIFO is full --- in all other
	 * situations, the write will complete without delay.
	 * </P>
	 * 
	 * @param buffer
	 *            The buffer that contains the data to write.
	 * @param offset
	 *            The offset within the buffer that the data starts.
	 * @param length
	 *            The number of bytes to try and write.
	 * @param timeoutMsec
	 *            See description. The number of milliseconds to wait for a
	 *            write to become possible.
	 * @return The number of bytes actually written.
	 * @throws IPCException
	 *             If the caller specified a wait time and this elapsed before
	 *             anything could be written, then this exception is thrown.
	 */
	public int write(byte[] buffer, int offset, int length, int timeoutMsec) throws IPCException
	{
		if (getDirection() != PipeDirection.Writer)
		{
			throw new IPCException("Attempt to write to a read-only pipe.");
		}

		FIFOResult result = new FIFOResult();
		writeImpl(result, buffer, offset, length, timeoutMsec);
		if (result.resultCode != FIFOResult.SUCCESS)
		{
			String msg = "Error writing named pipe, error code = " + result.errorCode;

			throw new IPCException(msg);
		}

		return result.byteCount;
	}

	private native void readImpl(FIFOResult result, byte[] buffer, int offset,
			int length, int timeoutMsec);

	public int read(byte[] buffer, int offset, int length) throws IPCException
	{
		return read(buffer, offset, length, -1);
	}
	
	
	public int read(byte[] buffer, int offset, int length, int timeoutMsec) throws IPCException
	{
		if (PipeDirection.Reader != getDirection())
		{
			throw new IPCException("Attempt to read a write-only pipe.");
		}

		FIFOResult result = new FIFOResult();
		readImpl(result, buffer, offset, length, timeoutMsec);

		if (result.resultCode == FIFOResult.ERROR_PIPE_CLOSED)
		{
			return -1;
		}
		
		if (result.resultCode != FIFOResult.SUCCESS)
		{
			String msg = "Error reading named pipe, code = " + result.errorCode;
			throw new IPCException(msg);
		}

		return result.byteCount;
	}

	private native void openImpl(FIFOResult result, int direction);

	public FIFOResult open(PipeDirection direction) throws IPCException
	{
		setDirection(direction);
		if (null == getActualName())
		{
			processVirtualActualFile();
		}
		FIFOResult result = new FIFOResult();
		openImpl(result, direction.jniValue);
		setHandle(result.handle);
		return result;
	}

	private String myActualName;

	private boolean myCreator;

	private PipeDirection myDirection;

	private long myHandle;

	private int myBufferSize;
	
	private String myVirtualName;
	
	public String getVirtualName()
	{
		return myVirtualName;
	}
	
	public void setVirtualName(String virtualName)
	{
		myVirtualName = virtualName;
	}

	protected int getBufferSize()
	{
		return myBufferSize;
	}

	protected void setBufferSize(int bufferSize)
	{
		myBufferSize = bufferSize;
	}

	protected String getActualName()
	{
		return myActualName;
	}

	protected PipeDirection getDirection()
	{
		return myDirection;
	}
	
	protected int getDirectionInt()
	{
		return myDirection.jniValue;
	}

	protected long getHandle()
	{
		return myHandle;
	}

	protected void setActualName(String actualName)
	{
		myActualName = actualName;
	}

	public void setCreator(boolean creator)
	{
		myCreator = creator;
	}

	protected void setDirection(PipeDirection direction)
	{
		myDirection = direction;
	}

	protected void setHandle(long handle)
	{
		myHandle = handle;
	}

	protected boolean isCreator()
	{
		return myCreator;
	}
	
	
	public native void createNonBlocking(FIFOResult result, String name);
	public native void readNonBlocking(FIFOResult result, byte[] buffer, int start, int length, int timeoutMsec);
	public native void writeNonBlocking(FIFOResult result, byte[] buffer, int start, int length, int timeoutMsec);

	/**
	 * Return a name that the underlying OS can use for a named pipe, given a string
	 * that is more or less unique.
	 * <P>
	 * Currently, all platforms that support named pipes use a file name to identify 
	 * the named pipe.  This method does several things.  First of all, it signals 
	 * to ECF IPC whether or not the OS uses special names for pipes.
	 * </P>
	 * <P>
	 * Windows, for example, requires that all named pipes occur in the directory, 
	 * "\\.\pipe" whereas linux has no such requirement.
	 * </P>
	 * <P>
	 * If the underlying OS has a special naming requirement, then it should return 
	 * a name that it can use, given a suffix string.
	 * </P>
	 * <P>
	 * On windows, for example, if one were to supply "123456" as the suffix, the method
	 * should return "\\.\pipe\123456"
	 * </P>
	 * <P>
	 * If the underlying OS does not have a special naming requirement, then it should 
	 * return null.  Therefore on linux, this method should return null.
	 * </P>
	 * 
	 * @param suffix A string that the actual name should be based on.  See description for 
	 * details.
	 * @return A suitable name if the platform requires that named pipes use a particular
	 * format, otherwise null.
	 */
	public static native String toActualName(String virtualName);
	
	/**
	 * Connect to the underlying FIFO.
	 */
	public native void connectNonBlocking(FIFOResult result);
	

	/**
	 * Determine if the FIFO is ready for reading, writing, or both.
	 * <P>
	 * This method is similar to the old Unix select system call, which would look 
	 * for file handles read to read/write.
	 * </P>
	 * <P>
	 * The call takes the amount of time the caller wants to wait for I/O to 
	 * be ready on the FIFO.  A value of less than 0 means wait until ready.  
	 * A value of 0 means poll the FIFO but do not wait.  Any other value is 
	 * the number of milliseconds to wait for the FIFO to be ready.
	 * </P>
	 * <P>
	 * If the client is the reader, then the method will wait until data is 
	 * ready.  If the client is a writer, it will wait for writing to become
	 * non-blocking.
	 * </P>
	 *   
	 * @param timeouitMsec
	 */
	public native void select(FIFOResult result, int timeoutMsec);
}
